// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedMember.Global

/* PdfWriter.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.IO;

using PdfSharpCore.Pdf.Advanced;
using PdfSharpCore.Pdf.Security;
using PdfSharpCore.Pdf.Internal;

#endregion

#nullable enable

namespace PdfSharpCore.Pdf.IO;

/// <summary>
/// Represents a writer for generation of PDF streams.
/// </summary>
internal class PdfWriter
{
    public PdfWriter
        (
            Stream pdfStream,
            PdfStandardSecurityHandler? securityHandler
        )
    {
        _stream = pdfStream;
        SecurityHandler = securityHandler;

        //System.Xml.XmlTextWriter
#if DEBUG
        _layout = PdfWriterLayout.Verbose;
#endif
    }

    public void Close (bool closeUnderlyingStream)
    {
        if (_stream != null! && closeUnderlyingStream)
        {
            _stream.Dispose();
        }

        _stream = null!;
    }

    public void Close()
    {
        Close (true);
    }

    public long Position => _stream.Position;

    /// <summary>
    /// Gets or sets the kind of layout.
    /// </summary>
    public PdfWriterLayout Layout
    {
        get => _layout;
        set => _layout = value;
    }

    PdfWriterLayout _layout;

    public PdfWriterOptions Options
    {
        get => _options;
        set => _options = value;
    }

    PdfWriterOptions _options;

    // -----------------------------------------------------------

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (bool value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value ? bool.TrueString : bool.FalseString);
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfBoolean value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.Value ? "true" : "false");
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (int value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.ToString (CultureInfo.InvariantCulture));
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (long value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.ToString (CultureInfo.InvariantCulture));
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (uint value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.ToString (CultureInfo.InvariantCulture));
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfInteger value)
    {
        WriteSeparator (CharCat.Character);
        _lastCat = CharCat.Character;
        WriteRaw (value.Value.ToString (CultureInfo.InvariantCulture));
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfLong value)
    {
        WriteSeparator (CharCat.Character);
        _lastCat = CharCat.Character;
        WriteRaw (value.Value.ToString (CultureInfo.InvariantCulture));
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfUInteger value)
    {
        WriteSeparator (CharCat.Character);
        _lastCat = CharCat.Character;
        WriteRaw (value.Value.ToString (CultureInfo.InvariantCulture));
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (double value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.ToString (Config.SignificantFigures7, CultureInfo.InvariantCulture));
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfReal value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.Value.ToString (Config.SignificantFigures7, CultureInfo.InvariantCulture));
        _lastCat = CharCat.Character;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfString value)
    {
        WriteSeparator (CharCat.Delimiter);
        PdfStringEncoding encoding = (PdfStringEncoding)(value.Flags & PdfStringFlags.EncodingMask);
        string pdf = (value.Flags & PdfStringFlags.HexLiteral) == 0
            ? PdfEncoders.ToStringLiteral (value.Value, encoding, SecurityHandler)
            : PdfEncoders.ToHexStringLiteral (value.Value, encoding, SecurityHandler);
        WriteRaw (pdf);

        _lastCat = CharCat.Delimiter;
    }

    /// <summary>
    /// Writes the specified value to the PDF stream.
    /// </summary>
    public void Write (PdfName value)
    {
        WriteSeparator (CharCat.Delimiter, '/');
        string name = value.Value;

        StringBuilder pdf = new StringBuilder ("/");
        for (int idx = 1; idx < name.Length; idx++)
        {
            char ch = name[idx];
            Debug.Assert (ch < 256);
            if (ch > ' ')
            {
                switch (ch)
                {
                    // TODO: is this all?
                    case '%':
                    case '/':
                    case '<':
                    case '>':
                    case '(':
                    case ')':
                    case '#':
                        break;

                    default:
                        pdf.Append (name[idx]);
                        continue;
                }
            }

            pdf.AppendFormat ("#{0:X2}", (int)name[idx]);
        }

        WriteRaw (pdf.ToString());
        _lastCat = CharCat.Character;
    }

    public void Write (PdfLiteral value)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (value.Value);
        _lastCat = CharCat.Character;
    }

    public void Write (PdfRectangle rect)
    {
        const string format = Config.SignificantFigures3;
        WriteSeparator (CharCat.Delimiter, '/');
        WriteRaw (PdfEncoders.Format (
            "[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "}]", rect.X1, rect.Y1,
            rect.X2, rect.Y2));
        _lastCat = CharCat.Delimiter;
    }

    public void Write (PdfReference iref)
    {
        WriteSeparator (CharCat.Character);
        WriteRaw (iref.ToString());
        _lastCat = CharCat.Character;
    }

    public void WriteDocString (string text, bool unicode)
    {
        WriteSeparator (CharCat.Delimiter);

        //WriteRaw(PdfEncoders.DocEncode(text, unicode));
        byte[] bytes;
        if (!unicode)
        {
            bytes = PdfEncoders.DocEncoding.GetBytes (text);
        }
        else
        {
            bytes = PdfEncoders.UnicodeEncoding.GetBytes (text);
        }

        bytes = PdfEncoders.FormatStringLiteral (bytes, unicode, true, false, SecurityHandler);
        Write (bytes);
        _lastCat = CharCat.Delimiter;
    }

    public void WriteDocString (string text)
    {
        WriteSeparator (CharCat.Delimiter);

        //WriteRaw(PdfEncoders.DocEncode(text, false));
        byte[] bytes = PdfEncoders.DocEncoding.GetBytes (text);
        bytes = PdfEncoders.FormatStringLiteral (bytes, false, false, false, SecurityHandler);
        Write (bytes);
        _lastCat = CharCat.Delimiter;
    }

    public void WriteDocStringHex (string text)
    {
        WriteSeparator (CharCat.Delimiter);

        //WriteRaw(PdfEncoders.DocEncodeHex(text));
        byte[] bytes = PdfEncoders.DocEncoding.GetBytes (text);
        bytes = PdfEncoders.FormatStringLiteral (bytes, false, false, true, SecurityHandler);
        _stream.Write (bytes, 0, bytes.Length);
        _lastCat = CharCat.Delimiter;
    }

    /// <summary>
    /// Begins a direct or indirect dictionary or array.
    /// </summary>
    public void WriteBeginObject (PdfObject obj)
    {
        bool indirect = obj.IsIndirect;
        if (indirect)
        {
            WriteObjectAddress (obj);
            if (SecurityHandler != null)
            {
                SecurityHandler.SetHashKey (obj.ObjectID);
            }
        }

        _stack.Add (new StackItem (obj));
        if (indirect)
        {
            if (obj is PdfArray)
            {
                WriteRaw ("[\n");
            }
            else if (obj is PdfDictionary)
            {
                WriteRaw ("<<\n");
            }

            _lastCat = CharCat.NewLine;
        }
        else
        {
            if (obj is PdfArray)
            {
                WriteSeparator (CharCat.Delimiter);
                WriteRaw ('[');
                _lastCat = CharCat.Delimiter;
            }
            else if (obj is PdfDictionary)
            {
                NewLine();
                WriteSeparator (CharCat.Delimiter);
                WriteRaw ("<<\n");
                _lastCat = CharCat.NewLine;
            }
        }

        if (_layout == PdfWriterLayout.Verbose)
        {
            IncreaseIndent();
        }
    }

    /// <summary>
    /// Ends a direct or indirect dictionary or array.
    /// </summary>
    public void WriteEndObject()
    {
        int count = _stack.Count;
        Debug.Assert (count > 0, "PdfWriter stack underflow.");

        StackItem stackItem = _stack[count - 1];
        _stack.RemoveAt (count - 1);

        PdfObject value = stackItem.Object;
        bool indirect = value.IsIndirect;
        if (_layout == PdfWriterLayout.Verbose)
        {
            DecreaseIndent();
        }

        if (value is PdfArray)
        {
            if (indirect)
            {
                WriteRaw ("\n]\n");
                _lastCat = CharCat.NewLine;
            }
            else
            {
                WriteRaw ("]");
                _lastCat = CharCat.Delimiter;
            }
        }
        else if (value is PdfDictionary)
        {
            if (indirect)
            {
                if (!stackItem.HasStream)
                {
                    WriteRaw (_lastCat == CharCat.NewLine ? ">>\n" : " >>\n");
                }
            }
            else
            {
                Debug.Assert (!stackItem.HasStream, "Direct object with stream??");
                WriteSeparator (CharCat.NewLine);
                WriteRaw (">>\n");
                _lastCat = CharCat.NewLine;
            }
        }

        if (indirect)
        {
            NewLine();
            WriteRaw ("endobj\n");
            if (_layout == PdfWriterLayout.Verbose)
            {
                WriteRaw (
                    "%--------------------------------------------------------------------------------------------------\n");
            }
        }
    }

    /// <summary>
    /// Writes the stream of the specified dictionary.
    /// </summary>
    public void WriteStream (PdfDictionary value, bool omitStream)
    {
        StackItem stackItem = _stack[_stack.Count - 1];
        Debug.Assert (stackItem.Object is PdfDictionary);
        Debug.Assert (stackItem.Object.IsIndirect);
        stackItem.HasStream = true;

        WriteRaw (_lastCat == CharCat.NewLine ? ">>\nstream\n" : " >>\nstream\n");

        if (omitStream)
        {
            WriteRaw ("  �...stream content omitted...�\n"); // useful for debugging only
        }
        else
        {
            byte[] bytes = value.Stream!.Value;
            if (bytes.Length != 0)
            {
                if (SecurityHandler != null)
                {
                    bytes = (byte[])bytes.Clone();
                    bytes = SecurityHandler.EncryptBytes (bytes)!;
                }

                Write (bytes);
                if (_lastCat != CharCat.NewLine)
                {
                    WriteRaw ('\n');
                }
            }
        }

        WriteRaw ("endstream\n");
    }

    public void WriteRaw (string rawString)
    {
        if (string.IsNullOrEmpty (rawString))
        {
            return;
        }

        byte[] bytes = PdfEncoders.RawEncoding.GetBytes (rawString);
        _stream.Write (bytes, 0, bytes.Length);
        _lastCat = GetCategory ((char)bytes[bytes.Length - 1]);
    }

    public void WriteRaw (char ch)
    {
        Debug.Assert (ch < 256, "Raw character greater than 255 detected.");

        _stream.WriteByte ((byte)ch);
        _lastCat = GetCategory (ch);
    }

    public void Write (byte[] bytes)
    {
        if (bytes == null || bytes.Length == 0)
        {
            return;
        }

        _stream.Write (bytes, 0, bytes.Length);
        _lastCat = GetCategory ((char)bytes[bytes.Length - 1]);
    }

    void WriteObjectAddress (PdfObject value)
    {
        if (_layout == PdfWriterLayout.Verbose)
        {
            WriteRaw (string.Format ("{0} {1} obj   % {2}\n",
                value.ObjectID.ObjectNumber, value.ObjectID.GenerationNumber,
                value.GetType().FullName));
        }
        else
        {
            WriteRaw (string.Format ("{0} {1} obj\n", value.ObjectID.ObjectNumber,
                value.ObjectID.GenerationNumber));
        }
    }

    public void WriteFileHeader (PdfDocument document)
    {
        StringBuilder header = new StringBuilder ("%PDF-");
        int version = document._version;
        header.Append ((version / 10).ToString (CultureInfo.InvariantCulture) + "." +
                       (version % 10).ToString (CultureInfo.InvariantCulture) + "\n%\xD3\xF4\xCC\xE1\n");
        WriteRaw (header.ToString());

        if (_layout == PdfWriterLayout.Verbose)
        {
            WriteRaw (string.Format ("% PDFsharp Version {0} (verbose mode)\n", VersionInfo.Version));

            // Keep some space for later fix-up.
            _commentPosition = (int)_stream.Position + 2;
            WriteRaw ("%                                                \n");
            WriteRaw ("%                                                \n");
            WriteRaw ("%                                                \n");
            WriteRaw ("%                                                \n");
            WriteRaw ("%                                                \n");
            WriteRaw (
                "%--------------------------------------------------------------------------------------------------\n");
        }
    }

    public void WriteEof (PdfDocument document, long startxref)
    {
        WriteRaw ("startxref\n");
        WriteRaw (startxref.ToString (CultureInfo.InvariantCulture));
        WriteRaw ("\n%%EOF\n");
        var fileSize = _stream.Position;
        if (_layout == PdfWriterLayout.Verbose)
        {
            TimeSpan duration = DateTime.Now - document._creation;

            _stream.Position = _commentPosition;

            // Without InvariantCulture parameter the following line fails if the current culture is e.g.
            // a Far East culture, because the date string contains non-ASCII characters.
            // So never never never never use ToString without a culture info.
            WriteRaw ("Creation date: " + document._creation.ToString ("G", CultureInfo.InvariantCulture));
            _stream.Position = _commentPosition + 50;
            WriteRaw ("Creation time: " + duration.TotalSeconds.ToString ("0.000", CultureInfo.InvariantCulture) +
                      " seconds");
            _stream.Position = _commentPosition + 100;
            WriteRaw ("File size: " + fileSize.ToString (CultureInfo.InvariantCulture) + " bytes");
            _stream.Position = _commentPosition + 150;
            WriteRaw ("Pages: " + document.Pages.Count.ToString (CultureInfo.InvariantCulture));
            _stream.Position = _commentPosition + 200;
            WriteRaw ("Objects: " + document._irefTable.ObjectTable.Count.ToString (CultureInfo.InvariantCulture));
        }
    }

    /// <summary>
    /// Gets or sets the indentation for a new indentation level.
    /// </summary>
    internal int Indent
    {
        get => _indent;
        set => _indent = value;
    }

    int _indent = 2;
    int _writeIndent;

    /// <summary>
    /// Increases indent level.
    /// </summary>
    void IncreaseIndent()
    {
        _writeIndent += _indent;
    }

    /// <summary>
    /// Decreases indent level.
    /// </summary>
    void DecreaseIndent()
    {
        _writeIndent -= _indent;
    }

    ///// <summary>
    ///// Returns an indent string of blanks.
    ///// </summary>
    //static string Ind(int _indent)
    //{
    //  return new String(' ', _indent);
    //}

    /// <summary>
    /// Gets an indent string of current indent.
    /// </summary>
    string IndentBlanks => new string (' ', _writeIndent);

    void WriteIndent()
    {
        WriteRaw (IndentBlanks);
    }

    void WriteSeparator (CharCat cat, char ch)
    {
        switch (_lastCat)
        {
            case CharCat.NewLine:
                if (_layout == PdfWriterLayout.Verbose)
                {
                    WriteIndent();
                }

                break;

            case CharCat.Delimiter:
                break;

            case CharCat.Character:
                if (_layout == PdfWriterLayout.Verbose)
                {
                    //if (cat == CharCat.Character || ch == '/')
                    _stream.WriteByte ((byte)' ');
                }
                else
                {
                    if (cat == CharCat.Character)
                    {
                        _stream.WriteByte ((byte)' ');
                    }
                }

                break;
        }
    }

    void WriteSeparator (CharCat cat)
    {
        WriteSeparator (cat, '\0');
    }

    public void NewLine()
    {
        if (_lastCat != CharCat.NewLine)
        {
            WriteRaw ('\n');
        }
    }

    CharCat GetCategory (char ch)
    {
        if (Lexer.IsDelimiter (ch))
        {
            return CharCat.Delimiter;
        }

        if (ch == Chars.LF)
        {
            return CharCat.NewLine;
        }

        return CharCat.Character;
    }

    enum CharCat
    {
        NewLine,
        Character,
        Delimiter,
    };

    CharCat _lastCat;

    /// <summary>
    /// Gets the underlying stream.
    /// </summary>
    internal Stream Stream => _stream;

    Stream _stream;

    internal PdfStandardSecurityHandler? SecurityHandler { get; set; }

    class StackItem
    {
        public StackItem (PdfObject value)
        {
            Object = value;
        }

        public readonly PdfObject Object;
        public bool HasStream;
    }

    readonly List<StackItem> _stack = new List<StackItem>();
    private int _commentPosition;
}
